Conversation
- EditCardProvider에 드래그앤드랍 관련 상태 추가 - 대시보드 편집 영역 상수 및 타입 정의 - 대시보드 카드 타입에 드래그 상태 및 고스트 상태 추가
- 드래그앤드랍에서 충돌 로직을 계산할 때 grid 배열 시 코드가 복잡해지기 때문에 수정함
- 대시보드에서 카드의 위치와 크기를 계산하는 훅을 추가함 - 카드를 absolute 포지션으로 배치하고, 마우스 위치를 통해 그리드 셀을 도출해내는 로직을 위해 필요
- PlusIconButton과 TrashCanIconButton에서 클릭 이벤트가 부모 요소로 전파되지 않도록 수정 - 사용자 경험 개선을 위해 버튼 클릭 시 의도한 동작만 수행하도록 함
- 카드 밀어내기 알고리즘 구현 - 그리드 셀에 카드 위치 계산 기능 추가 - 드래그 이벤트 핸들러 구현 - dx, dy로 된 방향 객체 리터럴 선언
- 카드 편집 뷰에 드래그 앤 드롭 이벤트 핸들러 추가 - 미니 뷰에 드래그 앤 드롭 관련 상태 및 핸들러 통합 - 드래그 중인 카드의 시각적 피드백을 위한 Ghost 컴포넌트 추가
- 스로틀링을 통해 불필요한 함수 호출 방지 - 새로운 스로틀 유틸리티 함수 추가
There was a problem hiding this comment.
Code Review
대시보드 카드 편집을 위한 드래그앤드롭 기능 구현을 확인했습니다. 2차원 배열로 관리되던 그리드 상태를 카드 객체 배열(placedCards)로 변경한 점은 데이터 모델링 관점에서 매우 훌륭한 리팩토링입니다. 이를 통해 상태 관리가 더 명확해지고 확장성이 개선되었습니다. 카드 밀어내기(push) 로직을 포함한 드래그앤드롭 구현은 복잡하지만, 관련 상태와 로직을 커스텀 훅(useDragAndDropCard, useGridCellSize)으로 분리하여 구조적으로 잘 설계되었습니다. 다만, useGridCellSize 훅에서 그리드의 크기를 하드코딩한 부분은 잠재적인 문제를 야기할 수 있어 개선이 필요합니다. 해당 부분에 대한 구체적인 피드백을 리뷰 코멘트로 남겼습니다.
|
마우스 위치와 그리드 셀 위치 간 상관관계를 구하는 데 있어서, 그리드 셀 사이즈가 반응형이면 코드 복잡도가 너무 증가할 것 같아 그리드 셀 사이즈를 고정했습니다. 이 부분에 대한 의견 부탁 드립니다. |
|
백엔드 카드 위치 DTO가 rowNo, colNo인데 프론트엔드 메트릭 카드 상수에서 사이즈는 sizeX, sizeY여서 로직 상에 row/col과 x/y가 혼용되어 있습니다. 보기에 불편하지 않는지 의견 부탁 드립니다. |
|
메모이제이션 최적화가 적용되지 않은 상태입니다. 따라서 ghost 렌더링마다 전체 목록이 리렌더링 되고 있습니다. 이후에 이슈를 새로 파서 작업할 생각인데 지금 작업이 필요한지에 대해 의견 부탁 드립니다. |
lee0jae330
left a comment
There was a problem hiding this comment.
복잡한 기능 구현하시느라 고생하셨습니다 !! 군데군데 주석처리된 부분 지워주세요 ! (디버깅용 console등)
frontend/src/components/dashboard/dashboard-edit/CardEditViewCard.tsx
Outdated
Show resolved
Hide resolved
| <EditCardContext.Provider | ||
| value={{ | ||
| initPlacedCards, | ||
| placedCards, | ||
| setPlacedCards, | ||
| gridRef, | ||
| dragState, | ||
| setDragState, | ||
| ghost, | ||
| setGhost, | ||
| tempLayout, | ||
| setTempLayout, | ||
| isOverList, | ||
| setIsOverList, | ||
| }} | ||
| > |
There was a problem hiding this comment.
p3: 이젠 진짜 state Provider, action Provider로 나누어야 할 것 같네요..
useReducer를 사용해서 state provider랑 dispatch provider를 분리하면 좋을 것 같습니다
아니면 context를 쪼갠다거나...
There was a problem hiding this comment.
지금 dispatch 함수만을 구독하고 있는 컴포넌트가 없습니다 (다 state만 구독하거나 state & dispatch를 동시에 구독하고 있어요)
그래서 state provider와 dispatch provider를 분리해서 얻을 이점이 딱히 없을 것 같습니다.
지금 보기에도 context에 상태가 너무 많아서 분리해야 할 필요성은 있을 것 같습니다. 일단 카드 그리드 상태와 드래그앤드랍 상태를 나눌 수 있을 것 같아요. 다만 카드목록 전체 리렌더링 상황에서도 selector를 고려했어서, 대시보드 카드 관련 내용을 다 외부 스토어로 전환하는 게 어떤가 싶습니다.
frontend/src/components/dashboard/dashboard-edit/MiniViewActiveCard.tsx
Outdated
Show resolved
Hide resolved
frontend/src/components/dashboard/dashboard-edit/MiniViewGhost.tsx
Outdated
Show resolved
Hide resolved
이 부분은 좋은데, 브라우저 너비가 작아지면, overflow-hidden처리는 문제가 있어서 해상도 가드를 적용하거나 overflow-scroll이 되어야할 것 같네요 |
최적화가 되면 좋을 것 같긴 하네요... |
이건 추후 맞추는것으로 할까요 ?? 기능 구현부터 해야할 것 같네요.... |
아직 눈에 띄는 문제는 없습니다. 카드 목록에 컴포넌트들이 다 들어오면 문제가 좀 있을 수 있겠네요. 리팩터링 이슈 파두겠습니다! |
|
백준 알고리즘 문제 같네요...ㅎㅎ 브랜치 fetch 땡겨서 직접 해봤는데 UX적으로 완성도 높은 구현이라는 생각이 들었습니다! 너무 수고하셨습니다!! |
overflow-scroll 적용했습니다. |
#️⃣ 변경 사항
대시보드 카드 편집 기능에 드래그 앤 드롭(Drag & Drop) 시스템을 도입하고, 효율적인 카드 배치를 위한 상태 관리 방식을 리팩터링했습니다. 기존의 2차원 배열 기반 그리드 관리 방식에서 카드 배열 기반의 좌표 관리 방식으로 전환하였으며, 카드 간 충돌 시 자동으로 위치를 조정하는 밀어내기 알고리즘을 구현했습니다.
#️⃣ 작업 상세 내용
드래그 앤 드롭(Drag & Drop) 핵심 로직 구현
useDragAndDropCard커스텀 훅을 신설하여 드래그 시작, 드래그 오버, 드롭 등 전반적인 D&D 이벤트 핸들러 통합 관리dragOver이벤트에 스로틀링(Throttling)을 적용하여 성능 최적화카드 밀어내기 재귀 알고리즘 구현
getPushedLayout: 드래그 중인 카드와 충돌하는 기존 카드들을 진입 방향에 따라 재귀적으로 밀어내는 알고리즘 구현getPushDirectionPriority: 드래그 중인 카드의 중심점과 충돌 카드의 위치를 비교하여 밀어낼 방향의 우선순위 결정상태 관리 및 데이터 구조 리팩터링
EditCardProvider: 2차원 배열((MetricCardCode | null)[][]) 대신 카드 정보 객체 배열(DashboardCard[])로 상태 관리 방식 변경EditCardContext: 드래그 상태(dragState), 고스트 상태(ghost), 임시 레이아웃(tempLayout) 관리를 위한 필드 추가 및gridRef를 통한 좌표 계산 기반 마련UI/UX 및 스타일링 개선
MiniViewGhost: 카드가 놓일 위치를 미리 보여주는 고스트 컴포넌트 추가 및 유효성(배치 가능 여부)에 따른 스타일링useGridCellSize: 컨테이너 크기를 기반으로 각 그리드 셀의 픽셀 좌표와 크기를 계산하는 훅 구현기타 개선 사항
PlusIconButton,TrashCanIconButton: 클릭 이벤트 전파 방지(stopPropagation) 처리useEditCard훅의 카드 추가/삭제 로직을 새로운 데이터 구조에 맞춰 수정카드 밀어내기 알고리즘 플로우
플로우 차트 확인
flowchart TD %% 최상단 시작점 Start([시작]) --> DragEvent["'onDragOver' 이벤트 수신"] subgraph "1. 전처리 단계" DragEvent --> Throttle["'Throttle' 적용 (100ms)"] Throttle --> CalcPos["'calculateGridPos': 마우스 → 그리드 {row, col} 변환"] CalcPos --> GhostCheck{"'ghost' 위치가 변경되었는가?"} end GhostCheck -- "아니오" --> End([종료]) GhostCheck -- "예" --> InitSim["시뮬레이션 레이아웃 구성<br/>(리스트 추가 시 새 카드 포함)"] subgraph "2. 재귀적 밀어내기 (getPushedLayout)" InitSim --> SetMoved["현재 카드를 'movedCards'에 추가"] SetMoved --> FindConflict["'getConflictingCards': 충돌 카드 검색"] FindConflict --> HasConflict{충돌 발생?} HasConflict -- "예" --> GetPriority["진입방향별 밀어낼 방향<br/>우선순위 결정<br/>({dx,dy}[]) getPushDirectionPriority"] GetPriority --> DirLoop["우선순위 방향 순회 시도"] %% 좌표 계산 로직 강조 섹션 subgraph "좌표 계산 상세 (Next Position)" DirLoop --> CalcNext["'conflictNextX/Y' 계산 시작"] CalcNext --> XDir{dx 방향 확인} XDir -- "dx > 0 (우측으로 밀어내기)" --> SetNextX_R["'conflictNextX' = current.colNo + current.sizeX"] XDir -- "dx < 0 (좌측으로 밀어내기)" --> SetNextX_L["'conflictNextX' = current.colNo - conflict.sizeX"] XDir -- "dx = 0" --> YDir SetNextX_R --> YDir{dy 방향 확인} SetNextX_L --> YDir YDir -- "dy > 0 (하단으로 밀어내기)" --> SetNextY_D["'conflictNextY' = current.rowNo + current.sizeY"] YDir -- "dy < 0 (상단으로 밀어내기)" --> SetNextY_U["'conflictNextY' = current.rowNo - conflict.sizeY"] YDir -- "dy = 0" --> BoundaryCheck SetNextY_D --> BoundaryCheck SetNextY_U --> BoundaryCheck end BoundaryCheck{"그리드 경계 내에 있는가?<br/>(1 <= X < COL / 1 <= Y < ROW)"} BoundaryCheck -- "예" --> RecursiveCall["'getPushedLayout' 재귀 호출<br/>(변경된 좌표의 conflictCard 기준)"] RecursiveCall --> Success{결과 유효?} Success -- "예" --> FinalConflict["최종 충돌 여부 재확인"] %% 실패 시 루프 백 Success -- "아니오" --> DirLoop BoundaryCheck -- "아니오" --> DirLoop end HasConflict -- "아니오" --> FinalConflict subgraph "3. 최종 반영" FinalConflict --> ApplyLayout["'setTempLayout(cards)' 반영"] ApplyLayout --> SetGhost["'setGhost(isValid)' 업데이트"] end SetGhost --> End %% 스타일링 style Start fill:#f3f4f6,stroke:#374151,stroke-width:2px style End fill:#f3f4f6,stroke:#374151,stroke-width:2px style RecursiveCall fill:#eff6ff,stroke:#1d4ed8,stroke-dasharray: 5 5 style CalcNext fill:#fff7ed,stroke:#c2410c,stroke-width:2px드래그 이벤트 핸들러 동작 시퀀스
sequenceDiagram title "드래그앤드랍 & 밀어내기 상세 시퀀스" participant User as "사용자" participant CardList as "CardEditView (리스트)" participant MiniView as "MiniView (그리드)" participant Hook as "useDragAndDropCard (훅)" participant Context as "EditCardContext (상태)" participant PushAlgo as "getPushedLayout (알고리즘)" rect rgb(240, 255, 240) Note over User, CardList: [시작] 리스트 또는 그리드에서 드래그 시작 alt "리스트에서 시작" User->>CardList: "'dragstart' (새 카드 선택)" CardList->>Hook: "'handleDragStart(e, LIST, card)'" else "그리드에서 시작" User->>MiniView: "'dragstart' (기존 카드 선택)" MiniView->>Hook: "'handleDragStart(e, GRID, card)'" Hook->>Context: "해당 카드의 'z-index' 낮춤 (숨김 처리)" end Hook->>Context: "'setDragState({ sourceArea, draggingCard, centerOffset })'" end loop "그리드 위에서 드래그 ('throttledHandleGridDragOver')" User->>MiniView: "'dragover' 이벤트 지속 발생" MiniView->>Hook: "'handleGridDragOver(e)'" Hook->>Hook: "'calculateGridPos' (마우스-셀 센터 비교)" Hook->>PushAlgo: "'getPushedLayout(currentLayout, ghostCandidate, ...)'" PushAlgo->>PushAlgo: "재귀적 충돌 계산 및 'PushPriority' 적용" PushAlgo-->>Hook: "결과: { cards, isValid }" Hook->>Context: "'setTempLayout(cards)', 'setGhost({row, col, isValid})'" Context-->>MiniView: "임시 레이아웃 및 'ghost' 시각화" end rect rgb(255, 245, 245) Note over User, CardList: [삭제 환경] 그리드 카드를 리스트로 드래그 User->>CardList: "'dragenter' (리스트 영역 진입)" CardList->>Hook: "'handleListDragEnter()'" Hook->>Context: "'setIsOverList(true)' -> '삭제하기' 오버레이 표시" end alt "그리드에 드롭 (이동 또는 추가)" User->>MiniView: "'drop' 이벤트 발생" MiniView->>Hook: "'handleGridDrop(e)'" Hook->>Context: "조건: 'ghost.isValid' 이면 'setPlacedCards(tempLayout)'" else "리스트에 드롭 (삭제)" User->>CardList: "'drop' 이벤트 발생" CardList->>Hook: "'handleListDrop(e)'" Hook->>Context: "조건: 'sourceArea === GRID' 이면 'setPlacedCards'에서 필터링 삭제" end Note over Hook, Context: [종료] 모든 드래그 상태 초기화 Hook->>Context: "'handleDragEnd()': dragState/ghost/tempLayout/isOverList 초기화"리뷰 참고사항
absolute로 두는 것으로 바꾸었습니다.grid: MetricCardCode[][]대신placedCards: DashboardCard[]를 관리하는 방식으로 변경하였습니다.#️⃣ 관련 이슈
📸 스크린샷 (선택)
2026-02-16.3.58.09.mov
📎 참고할만한 자료 (선택)
위키 작성 중에 있습니다. 아주 러프하게 써두었는데 트러블슈팅 목록을 정리해두었으니 궁금하시면 참고해주세요.